// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.neuronbit.businessflow.intellij.editor.swing.editor.propertyInspector.properties;

import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.ide.util.ClassFilter;
import com.intellij.ide.util.TreeClassChooser;
import com.intellij.ide.util.TreeClassChooserFactory;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonShortcuts;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.ui.ComponentWithBrowseButton;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.psi.*;
import com.intellij.psi.impl.java.stubs.index.JavaFullClassNameIndex;
import com.intellij.psi.impl.java.stubs.index.JavaMethodNameIndex;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.ui.EditorTextField;
import com.intellij.util.concurrency.AppExecutorUtil;
import com.neuronbit.businessflow.intellij.editor.UIDesignerBundle;
import com.neuronbit.businessflow.intellij.editor.swing.editor.FormEditingUtil;
import com.neuronbit.businessflow.intellij.editor.swing.editor.RadComponent;
import com.neuronbit.businessflow.intellij.editor.swing.editor.RadJavaMethodComponent;
import com.neuronbit.businessflow.intellij.editor.swing.editor.propertyInspector.InplaceContext;
import com.neuronbit.businessflow.intellij.editor.swing.editor.propertyInspector.Property;
import com.neuronbit.businessflow.intellij.editor.swing.editor.propertyInspector.PropertyEditor;
import com.neuronbit.businessflow.intellij.editor.swing.editor.propertyInspector.PropertyRenderer;
import com.neuronbit.businessflow.intellij.editor.swing.editor.propertyInspector.renderers.LabelPropertyRenderer;
import com.neuronbit.businessflow.intellij.model.FormattedMethodString;
import com.neuronbit.businessflow.intellij.util.PsiUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.concurrency.CancellablePromise;

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Collection;
import java.util.concurrent.ExecutionException;

/**
 * The type Method return type property.
 */
public final class CallableMethodReturnTypeProperty extends Property<RadJavaMethodComponent, String> {
    private final PropertyRenderer myRenderer;
    private final PropertyEditor myEditor;

    /**
     * Instantiates a new Method return type property.
     *
     * @param component the component
     * @param project   the project
     * @param name      the name
     */
    public CallableMethodReturnTypeProperty(RadJavaMethodComponent component, final Project project, final String name) {
        super(null, name);
        myRenderer = new LabelPropertyRenderer(UIDesignerBundle.message("client.properties.configure"));
        myEditor = new ChangeMethodSignaturePropertyEditor(component);
    }

    @Override
    public PropertyEditor<String> getEditor() {
        return myEditor;
    }

    @Override
    @NotNull
    public PropertyRenderer<String> getRenderer() {
        return myRenderer;
    }

    @Override
    public String getValue(final RadJavaMethodComponent component) {
//        if (component.getMethodToBind() != null) {
//            return StringUtils.substringAfter(component.getMethodToBind(), ":");
//        } else {
//            return null;
//        }
        //TODO shunyun 2024/4/2: 读取 psi，匹配到 method，再获取 return type
        return getReturnTypeName(component);
    }

    private PsiType getReturnType(RadJavaMethodComponent component) {
        if (StringUtils.isBlank(component.getClassToBind())||component.getMethodToBind() == null || component.getMethodToBind().isBlank()) {
            return null;
        }
        Project project = component.getProject();
        CancellablePromise<PsiType> submitted = ReadAction.nonBlocking(() -> {
            Collection<PsiClass> classes = JavaFullClassNameIndex.getInstance()
                    .getClasses(component.getMethodToBind().getReturnType(), project, GlobalSearchScope.allScope(project));
            if (classes.isEmpty()) {
                return null;
            }

            for (PsiClass aClass : classes) {
                 return PsiUtils.getPsiTypeFromPsiClass(aClass);
            }
            return null;
        }).inSmartMode(project).submit(AppExecutorUtil.getAppExecutorService());

        try {
            return submitted.get();
        } catch (InterruptedException | ExecutionException e) {
            return null;
        }
    }

    private String getReturnTypeName(RadJavaMethodComponent component) {
        if (StringUtils.isBlank(component.getClassToBind())||component.getMethodToBind() == null || component.getMethodToBind().isBlank()) {
            return "";
        }
        return component.getMethodToBind().getReturnType();
    }

    @Override
    protected void setValueImpl(final RadJavaMethodComponent component, final String value) {
        //TODO shunyun 2024/4/2: it should be readonly property, change the return type by method refactoring
        String className = value;

        if (className != null && className.length() == 0) {
            className = null;
        }
        if (className == null) {
            return;
        }
        final PsiClass aClass = FormEditingUtil.findClassToBind(component.getModule(), className);
        if (aClass == null) {
            return;
        }
        ApplicationManager.getApplication().runWriteAction(() -> {
            PsiClass clazz = FormEditingUtil.findClassToBind(component.getModule(), component.getClassToBind());
            if (clazz == null) {
                return;
            }
            FormattedMethodString methodToBind = component.getMethodToBind();
            String methodName = methodToBind.getName();
            PsiMethod[] psiMethods = clazz.findMethodsByName(methodName, true);
            if (ArrayUtils.isNotEmpty(psiMethods)) {
                for (PsiMethod psiMethod : psiMethods) {
                    if (PsiUtils.formatMethod(psiMethod).equals(methodToBind.getValue())) {
                        PsiElementFactory factory = PsiElementFactory.getInstance(psiMethod.getProject());
                        PsiTypeElement returnTypeElement = psiMethod.getReturnTypeElement();
                        if (returnTypeElement != null) {
                            returnTypeElement.replace(factory.createTypeElement(factory.createType(aClass)));
                            component.setMethodToBind(PsiUtils.formatMethod(psiMethod));
                        }
                    }
                }

            }
        });
    }

    @Override
    public Property @NotNull [] getChildren(RadJavaMethodComponent component) {
        PsiType returnType = getReturnType(component);
        if (null == returnType) {
            return EMPTY_ARRAY;
        }

        String returnTypeSimpleName = ReadAction.compute(returnType::getPresentableText);

        if (component.isFlow()) {
            return new Property[]{new FlowReturnTypeProperty(this, component.getProject(), "output", returnType)};
        } else {
            return EMPTY_ARRAY;
            //return new Property[]{new TaskReturnTypeProperty(this, component.getProject(), "output", returnType, component.getFlowReturnType(), returnTypeSimpleName)};
        }
    }

    private static final class MyEditor extends PropertyEditor<String> {
        private final EditorTextField myEditorTextField;
        private Document myDocument;
        private final ComponentWithBrowseButton<EditorTextField> myTfWithButton;
        private String myInitialValue;
        private final Project myProject;
        private final MyActionListener myActionListener;

        /**
         * Instantiates a new My editor.
         *
         * @param project the project
         */
        MyEditor(final Project project) {
            myProject = project;
            myEditorTextField = new EditorTextField("", project, JavaFileType.INSTANCE) {
                @Override
                protected boolean shouldHaveBorder() {
                    return false;
                }
            };
            myActionListener = new MyActionListener();
            myTfWithButton = new ComponentWithBrowseButton<>(myEditorTextField, myActionListener);
            myEditorTextField.setBorder(null);
            new MyCancelEditingAction().registerCustomShortcutSet(CommonShortcuts.ESCAPE, myTfWithButton);
      /*
      myEditorTextField.addActionListener(
        new ActionListener() {
          public void actionPerformed(final ActionEvent e) {
            fireValueCommitted();
          }
        }
      );
      */
        }

        @Override
        public String getValue() {
            final String value = myDocument.getText();
            if (value.length() == 0 && myInitialValue == null) {
                return null;
            }
            return value.replace('$', '.'); // PSI works only with dots
        }

        @Override
        public JComponent getComponent(final RadComponent component, final String value, final InplaceContext inplaceContext) {
            myInitialValue = value;
            setEditorText(value != null ? value : "");
            myActionListener.setComponent(component);
            return myTfWithButton;
        }

        private void setEditorText(final String s) {
            final JavaCodeFragmentFactory factory = JavaCodeFragmentFactory.getInstance(myProject);
            PsiPackage defaultPackage = JavaPsiFacade.getInstance(myProject).findPackage("");
            final PsiCodeFragment fragment = factory.createReferenceCodeFragment(s, defaultPackage, true, true);
            myDocument = PsiDocumentManager.getInstance(myProject).getDocument(fragment);
            myEditorTextField.setDocument(myDocument);
        }

        @Override
        public void updateUI() {
            SwingUtilities.updateComponentTreeUI(myTfWithButton);
        }

        private final class MyActionListener implements ActionListener {
            /**
             * The My component.
             */
            RadComponent myComponent;

            /**
             * Sets component.
             *
             * @param component the component
             */
            public void setComponent(RadComponent component) {
                myComponent = component;
            }

            @Override
            public void actionPerformed(final ActionEvent e) {
                final String className = myEditorTextField.getText();
                final PsiClass aClass = FormEditingUtil.findClassToBind(myComponent.getModule(), className);

                final Project project = myComponent.getProject();
                final ProjectFileIndex fileIndex = ProjectRootManager.getInstance(project).getFileIndex();
                final TreeClassChooser chooser = TreeClassChooserFactory.getInstance(project).createWithInnerClassesScopeChooser(
                        UIDesignerBundle.message("title.choose.class.to.bind"),
                        GlobalSearchScope.allScope(project),
                        new ClassFilter() { // we need show classes from the sources roots only
                            @Override
                            public boolean isAccepted(final PsiClass aClass) {
                                return true;
//                                final VirtualFile vFile = aClass.getContainingFile().getVirtualFile();
//                                return vFile != null && fileIndex.isUnderSourceRootOfType(vFile, JavaModuleSourceRootTypes.SOURCES);
                            }
                        },
                        aClass
                );
                chooser.showDialog();

                final PsiClass result = chooser.getSelected();
                if (result != null) {
                    setEditorText(result.getQualifiedName());
                }

                // todo[anton] make it via providing proper parent
                IdeFocusManager.getGlobalInstance().doWhenFocusSettlesDown(() -> IdeFocusManager.getGlobalInstance().requestFocus(myEditorTextField, true));
            }
        }

        private final class MyCancelEditingAction extends AnAction {
            @Override
            public void actionPerformed(@NotNull final AnActionEvent e) {
                fireEditingCancelled();
            }
        }
    }
}
